/*jslint browser: true, unparam: true, todo: true*/
/*globals HTMLElement: false, Reflect: false, define: true, MutationObserver: false, requestAnimationFrame: false, performance: false, btoa: false*/
'use strict';

import { createHTMLString, createTextString } from './events/util';

export default function (self, ctor) {
  self.scale = 1;
  self.orders = {
    rows: [],
    columns: [],
  };
  self.appliedInlineStyles = {};
  self.cellGridAttributes = {};
  self.treeGridAttributes = {};
  self.visibleRowHeights = [];
  self.hasFocus = false;
  self.activeCell = {
    columnIndex: 0,
    rowIndex: 0,
  };
  self.innerHTML = '';
  self.storageName = 'canvasDataGrid';
  self.invalidSearchExpClass = 'canvas-datagrid-invalid-search-regExp';
  self.localStyleLibraryStorageKey = 'canvas-datagrid-user-style-library';
  self.dataType = 'application/x-canvas-datagrid';
  self.orderBy = null;
  self.orderDirection = 'asc';
  self.orderings = {
    columns: [],
    add: function (orderBy, orderDirection, sortFunction) {
      self.orderings.columns = self.orderings.columns.filter(function (col) {
        return col.orderBy !== orderBy;
      });
      self.orderings.columns.push({
        orderBy: orderBy,
        orderDirection: orderDirection,
        sortFunction: sortFunction,
      });
    },
    sort: function () {
      console.warn(
        'grid.orderings.sort has been deprecated. Please use grid.refresh().',
      );

      self.orderings.columns.forEach(function (col) {
        self.viewData.sort(col.sortFunction(col.orderBy, col.orderDirection));
      });
    },
  };
  self.columnFilters = {};
  self.filters = {};
  self.frozenRow = 0;
  self.frozenColumn = 0;
  self.ellipsisCache = {};
  self.scrollCache = { x: [], y: [] };
  self.scrollBox = {};
  self.visibleRows = [];
  self.visibleCells = [];
  /**
   * Each item of this  array contains these properties:
   * - `x`, `y`, `x2`, `y2`
   * - `orderIndex0`, `orderIndex1`: The closed interval of the hiding rows/columns.
   * - `dir`: The directon of the unhide indicator. 'l' and 'r' for columns, 't' and 'b' for rows
   */
  self.visibleUnhideIndicators = [];
  /**
   * Each item is a tuple conatins two numbers:
   * its type difination: Array<[beginRowIndex, endRowIndex]>
   * Each tuple represents a closed Interval
   */
  self.hiddenRowRanges = [];
  /**
   * This array stored all groups information with context for drawing,
   * it is generated by drawing functions,
   * and be used for searching groups when users operate on the spreadsheet
   * Each item of this array contains these properties:
   * - `type`: its available values: 'c' and 'r'. indicates the type of this item, 'c' for column group
   *           and 'r' for row group.
   * - `x`,`y`: the left-top point of this group's rendering area.
   * - `x2`, `y2`: the right-bottom of this group's rendering area.
   * - `collapsed`: this value indicates the collapsed status of this group.
   * - `from`, `to`: The column index range of this group (We use this value for searching the group)
   * - `row`: The row index for column groups (We use this value for searching the group)
   */
  self.visibleGroups = [];
  self.sizes = {
    rows: {},
    columns: {},
    trees: {},
  };
  self.fillOverlay = {};
  self.filterable = {
    rows: [],
    columns: [],
  };
  self.selectedFilterButton = {
    columnIndex: -1,
    rowIndex: -1,
  };
  self.cellTree = {
    rows: [],
    columns: {},
    tempSchema: {},
    rowTreeColIndex: 0,
    columnTreeRowStartIndex: 0,
    columnTreeRowEndIndex: 0,
    origin: {
      rows: [],
      columns: {},
    },
  };
  self.hovers = {};
  self.attributes = {};
  self.style = {};
  self.formatters = {};
  self.sorters = {};
  self.parsers = {};
  self.schemaHashes = {};
  self.events = {};
  self.changes = [];
  self.scrollIndexTop = 0;
  self.scrollPixelTop = 0;
  self.scrollIndexLeft = 0;
  self.scrollPixelLeft = 0;
  self.childGrids = {};
  self.openChildren = {};

  /**
   * Array for grouped columns
   * Each item in this array is an array and it represents some grouping in one row
   * A grouping descriptor has three properties:
   * - `from`: The column index of the first column
   * - `to`: The column index of the last column
   * - `collapsed`: Is this group be collapsed
   * @example [[{ from: 1, to: 2, collapsed: false }]]
   */
  self.groupedColumns = [];

  /**
   * Array for grouped rows
   * Each item in this array is an array and it represents some grouping in one column
   * A grouping descriptor has three properties:
   * - `from`: The row index of the first row
   * - `to`: The row index of the last row
   * - `collapsed`: Is this group be collapsed
   * @example [[{ from: 1, to: 2, collapsed: false }]]
   */
  self.groupedRows = [];

  self.scrollModes = [
    'vertical-scroll-box',
    'vertical-scroll-top',
    'vertical-scroll-bottom',
    'horizontal-scroll-box',
    'horizontal-scroll-right',
    'horizontal-scroll-left',
  ];
  self.componentL1Events = {};
  self.eventNames = [
    'afterdraw',
    'afterrendercell',
    'afterrenderfilterbutton',
    'aftercreategroup',
    'attributechanged',
    'beforebeginedit',
    'beforecreatecellgrid',
    'beforedraw',
    'beforeendedit',
    'beforerendercell',
    'beforerendercellgrid',
    'beforerenderfilterbutton',
    'beginedit',
    'cellmouseout',
    'cellmouseover',
    'click',
    'collapsetree',
    'columnhide',
    'columnunhide',
    'contextmenu',
    'copy',
    'datachanged',
    'dblclick',
    'endedit',
    'expandtree',
    'formatcellvalue',
    'keydown',
    'keypress',
    'keyup',
    'mousedown',
    'mousemove',
    'mouseup',
    'newrow',
    'ordercolumn',
    'rendercell',
    'rendercellgrid',
    'renderorderbyarrow',
    'rendertext',
    'rendertreearrow',
    'reorder',
    'reordering',
    'resize',
    'resizecolumn',
    'resizerow',
    'schemachanged',
    'scroll',
    'selectionchanged',
    'stylechanged',
    'touchcancel',
    'touchend',
    'touchmove',
    'touchstart',
    'wheel',
  ];
  self.mouse = { x: 0, y: 0 };

  self.getBoundRowIndexFromViewRowIndex = function (viewRowIndex) {
    if (self.boundRowIndexMap && self.boundRowIndexMap.has(viewRowIndex)) {
      return self.boundRowIndexMap.get(viewRowIndex);
    }

    return undefined;
  };
  self.getBoundColumnIndexFromViewColumnIndex = function (viewColumnIndex) {
    return self.orders.columns[viewColumnIndex];
  };
  /**
   * Get the height of the area about column groups for rendering and handling events.
   */
  self.getColumnGroupAreaHeight = function () {
    if (!self.attributes.allowGroupingColumns) {
      return 0;
    }
    const groups = self.groupedColumns.length;
    const base = self.style.columnGroupRowHeight * self.scale;
    return base * groups;
  };
  /**
   * Get the width of the area about row groups for rendering and handling events.
   */
  self.getRowGroupAreaWidth = function () {
    if (!self.attributes.allowGroupingRows) {
      return 0;
    }
    const groups = self.groupedRows.length;
    const base = self.style.rowGroupColumnWidth * self.scale;
    return base * groups;
  };
  self.getCollapsedColumnGroups = function () {
    const result = [];
    for (let rowIndex = 0; rowIndex < self.groupedColumns.length; rowIndex++) {
      const groups = self.groupedColumns[rowIndex];
      for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
        const group = groups[groupIndex];
        if (group.collapsed) result.push(group);
      }
    }
    return result;
  };
  self.getCollapsedRowGroups = function () {
    const result = [];
    for (let rowIndex = 0; rowIndex < self.groupedRows.length; rowIndex++) {
      const groups = self.groupedRows[rowIndex];
      for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
        const group = groups[groupIndex];
        if (group.collapsed) result.push(group);
      }
    }
    return result;
  };
  /**
   * Toggle the collapse status of a group (expanded/collapsed)
   * @param {{type:string,from:number,to:number}} group
   */
  self.toggleGroup = function (group) {
    if (group.type === 'c') {
      const { from, to } = group;
      /** @type {{from:number,to:number,collapsed:boolean}} */
      let matchedGroup;
      /** @type {Array<Array<{from:number,to:number,collapsed:boolean}>>} */
      const allGroups = self.groupedColumns;
      for (let i = 0; i < allGroups.length; i++) {
        const groups = allGroups[i];
        for (let gi = 0; gi < groups.length; gi++) {
          const group = groups[gi];
          if (group.from === from && group.to === to) {
            matchedGroup = group;
            break;
          }
        }
        if (matchedGroup) break;
      }
      if (!matchedGroup) return;
      const nextCollapsed = !matchedGroup.collapsed;
      matchedGroup.collapsed = nextCollapsed;
      return true;
    }
    if (group.type === 'r') {
      const { from, to } = group;
      /** @type {{from:number,to:number,collapsed:boolean}} */
      let matchedGroup;
      /** @type {Array<Array<{from:number,to:number,collapsed:boolean}>>} */
      const allGroups = self.groupedRows;
      for (let i = 0; i < allGroups.length; i++) {
        const groups = allGroups[i];
        for (let gi = 0; gi < groups.length; gi++) {
          const group = groups[gi];
          if (group.from === from && group.to === to) {
            matchedGroup = group;
            break;
          }
        }
        if (matchedGroup) break;
      }
      if (!matchedGroup) return;
      const nextCollapsed = !matchedGroup.collapsed;
      matchedGroup.collapsed = nextCollapsed;
      return true;
    }
    return false;
  };
  self.isNewGroupRangeValid = function (groupsArray, from, to) {
    for (let i = 0; i < groupsArray.length; i++) {
      const groups = groupsArray[i];
      for (let gIndex = 0; gIndex < groups.length; gIndex++) {
        const group = groups[gIndex];
        if (from === group.to + 1) return false;
        if (from > group.to) continue;

        if (from === group.from) {
          if (to === group.to) return false;
          if (to > group.to) return true;
          break; // check smaller range
        }
        if (from > group.from) {
          if (to > group.to) return false;
          break; // check smaller range
        }
        if (to < group.to) return false;
        return true;
      }
    }
    return true;
  };
  self.getColumnHeaderCellHeight = function () {
    if (!self.attributes.showColumnHeaders) {
      return 0;
    }
    return (
      (self.sizes.rows[-1] || self.style.columnHeaderCellHeight) * self.scale
    );
  };
  self.getRowHeaderCellWidth = function () {
    if (!self.attributes.showRowHeaders) {
      return 0;
    }
    return (
      (self.sizes.columns[-1] || self.style.rowHeaderCellWidth) * self.scale
    );
  };
  self.setStorageData = function () {
    if (!self.attributes.saveAppearance || !self.attributes.name) {
      return;
    }
    var visibility = {};
    self.getSchema().forEach(function (column) {
      visibility[column.name] = !column.hidden;
    });
    localStorage.setItem(
      self.storageName + '-' + self.attributes.name,
      JSON.stringify({
        sizes: {
          rows: self.sizes.rows,
          columns: self.sizes.columns,
        },
        orders: {
          rows: self.orders.rows,
          columns: self.orders.columns,
        },
        orderBy: self.orderBy,
        orderDirection: self.orderDirection,
        visibility: visibility,
      }),
    );
  };
  self.getSchema = function () {
    return self.schema || self.tempSchema || [];
  };
  function fillArray(low, high) {
    var i = [],
      x;
    for (x = low; x <= high; x += 1) {
      i[x] = x;
    }
    return i;
  }
  self.createColumnOrders = function () {
    var s = self.getSchema();
    self.orders.columns = fillArray(0, s.length - 1);
  };
  self.createRowOrders = function () {
    self.orders.rows = fillArray(0, self.originalData.length - 1);
  };
  self.getVisibleSchema = function () {
    return self.getSchema().filter(function (col) {
      return !col.hidden;
    });
  };
  self.applyDefaultValue = function (row, header, rowIndex) {
    var d = header.defaultValue || '';
    if (typeof d === 'function') {
      d = d.apply(self.intf, [header, rowIndex]);
    }
    row[header.name] = d;
  };
  self.createNewRowData = function () {
    self.newRow = {};

    // The third argument of applyDefaultValue is the row index
    // of the row for which to apply the default value. In this
    // case, we're creating a new row but not yet appending it
    // to self.originalData, so no row index exists
    const newRowIndex = undefined;

    self.getSchema().forEach(function forEachHeader(header) {
      self.applyDefaultValue(self.newRow, header, newRowIndex);
    });
  };
  self.getSchemaNameHash = function (key) {
    var n = 0;
    while (self.schemaHashes[key]) {
      n += 1;
      key = key + n;
    }
    return key;
  };
  self.filter = function (type) {
    var f = self.filters[type];
    if (!f && type !== undefined) {
      console.warn(
        'Cannot find filter for type %s, falling back to substring match.',
        type,
      );
      f = self.filters.string;
    }
    return f;
  };
  self.hasActiveFilters = function () {
    return self.columnFilters && Object.keys(self.columnFilters).length > 0;
  };
  self.hasCollapsedRowGroup = function () {
    for (let i = 0; i < self.groupedRows.length; i++) {
      const groups = self.groupedRows[i];
      for (let j = 0; j < groups.length; j++) {
        const g = groups[j];
        if (g.collapsed) return true;
      }
    }
    return false;
  };
  self.getFilteredAndSortedViewData = function (originalData) {
    // We make a copy of originalData here in order be able to
    // filter and sort rows without modifying the original array.
    // Each row is turned into a (row, rowIndex) tuple
    // so that when we apply filters, we can refer back to the
    // row's original row number in originalData. This becomes
    // useful when emitting cell events.
    let newViewData = originalData.map((row, originalRowIndex) => [
      row,
      originalRowIndex,
    ]);

    // Remove hidden rows here. So we can keep the bound indexes correct
    if (self.hiddenRowRanges.length > 0) {
      const ranges = self.hiddenRowRanges.sort((a, b) => b[1] - a[1]);
      for (let i = 0; i < ranges.length; i++) {
        const [beginRowIndex, endRowIndex] = ranges[i];
        const countOfRows = endRowIndex - beginRowIndex + 1;
        newViewData.splice(beginRowIndex, countOfRows);
      }
    }

    // Apply filtering
    for (const [headerName, filterText] of Object.entries(self.columnFilters)) {
      const header = self.getHeaderByName(headerName);

      if (!header) {
        continue;
      }

      const currentFilterFunction =
        header.filter || self.filter(header.type || 'string');

      newViewData = newViewData.filter(function ([row, originalRowIndex]) {
        if (
          self.attributes.allowFreezingRows &&
          !self.attributes.filterFrozenRows &&
          originalRowIndex < self.frozenRow
        )
          return true;

        return currentFilterFunction(row[headerName], filterText);
      });
    }

    //#region Hide rows from collapsed group
    /** @type {number[][]} */
    let collapsedGroups = [];
    for (let i = 0; i < self.groupedRows.length; i++) {
      const rows = self.groupedRows[i];
      for (let j = 0; j < rows.length; j++) {
        const r = rows[j];
        if (!r.collapsed) continue;
        collapsedGroups.push([r.from, r.to]);
      }
    }
    if (collapsedGroups.length > 0) {
      //#region merge groups
      collapsedGroups.sort((a, b) => a[0] - b[0]);
      let newLen = 0;
      const len = collapsedGroups.length;
      for (let i = 0; i < len; i++) {
        const r = collapsedGroups[i];
        if (i === len - 1) {
          collapsedGroups[newLen++] = r;
          break;
        }
        const to = r[1];
        const [from2, to2] = collapsedGroups[i + 1];
        if (from2 > to + 1) {
          collapsedGroups[newLen++] = r;
          continue;
        }
        collapsedGroups[i + 1] = r;
        if (to2 > to) collapsedGroups[i + 1][1] = to2;
      }
      collapsedGroups = collapsedGroups.slice(0, newLen);
      //#endregion merge groups

      //#region omit rows by groups
      let g = collapsedGroups.shift();
      for (let start = 0; start < newViewData.length; start++) {
        const it = newViewData[start][1];
        if (it < g[0]) continue;
        let end = start + 1;
        for (; end < newViewData.length; end++) {
          const it2 = newViewData[end][1];
          if (it2 > g[1]) break;
        }
        newViewData.splice(start, end - start);
        g = collapsedGroups.shift();
        if (!g) break;
        start--;
      }
      //#endregion omit rows by groups
    }
    //#endregion Hide rows from collapsed group

    // Apply sorting
    for (const column of self.orderings.columns) {
      const sortFn = column.sortFunction(column.orderBy, column.orderDirection);

      newViewData.sort(([rowA], [rowB, rowIndexB]) => {
        if (
          self.attributes.allowFreezingRows &&
          !self.attributes.sortFrozenRows &&
          rowIndexB < self.frozenRow
        )
          return 0;
        return sortFn(rowA, rowB);
      });
    }

    return {
      viewData: newViewData.map(([row]) => row),
      boundRowIndexMap: new Map(
        newViewData.map(([_row, originalRowIndex], viewRowIndex) => [
          viewRowIndex,
          originalRowIndex,
        ]),
      ),
    };
  };
  self.refresh = function () {
    const { viewData, boundRowIndexMap } = self.getFilteredAndSortedViewData(
      self.originalData,
    );

    self.viewData = viewData;
    self.boundRowIndexMap = boundRowIndexMap;

    self.resize();
    self.draw(true);
  };
  self.getBestGuessDataType = function (columnName, data) {
    var t,
      x,
      l = data.length;
    for (x = 0; x < l; x += 1) {
      if (
        data[x] !== undefined &&
        data[x] !== null &&
        [null, undefined].indexOf(data[x][columnName]) !== -1
      ) {
        t = typeof data[x];
        return t === 'object' ? 'string' : t;
      }
    }
    return 'string';
  };
  self.drawChildGrids = function () {
    Object.keys(self.childGrids).forEach(function (gridKey) {
      self.childGrids[gridKey].draw();
    });
  };
  self.resizeChildGrids = function () {
    Object.keys(self.childGrids).forEach(function (gridKey) {
      self.childGrids[gridKey].resize();
    });
  };
  self.autoScrollZone = function (e, x, y, ctrl) {
    var setTimer,
      rowHeaderCellWidth = self.getRowHeaderCellWidth(),
      columnHeaderCellHeight = self.getColumnHeaderCellHeight();
    if (x !== -1) {
      if (x > self.width - self.attributes.selectionScrollZone) {
        self.scrollBox.scrollLeft += self.attributes.selectionScrollIncrement;
        setTimer = true;
      }
      if (x - self.attributes.selectionScrollZone - rowHeaderCellWidth < 0) {
        self.scrollBox.scrollLeft -= self.attributes.selectionScrollIncrement;
        setTimer = true;
      }
    }
    if (y !== -1) {
      if (y > self.height - self.attributes.selectionScrollZone) {
        self.scrollBox.scrollTop += self.attributes.selectionScrollIncrement;
        setTimer = true;
      }
      if (
        y - self.attributes.selectionScrollZone - columnHeaderCellHeight <
        0
      ) {
        self.scrollBox.scrollTop -= self.attributes.selectionScrollIncrement;
        setTimer = true;
      }
    }
    if (
      setTimer &&
      !ctrl &&
      self.currentCell &&
      self.currentCell.columnIndex !== -1
    ) {
      self.scrollTimer = setTimeout(
        self.mousemove,
        self.attributes.scrollRepeatRate,
        e,
      );
    }
  };
  self.validateColumn = function (c, s) {
    if (!c.name) {
      throw new Error('A column must contain at least a name.');
    }
    if (
      s.filter(function (i) {
        return i.name === c.name;
      }).length > 0
    ) {
      throw new Error(
        'A column with the name ' +
          c.name +
          ' already exists and cannot be added again.',
      );
    }
    return true;
  };
  self.setDefaults = function (obj1, obj2, key, def) {
    obj1[key] = obj2[key] === undefined ? def : obj2[key];
  };
  self.setAttributes = function () {
    self.defaults.attributes.forEach(function eachAttribute(i) {
      self.setDefaults(self.attributes, self.args, i[0], i[1]);
    });
  };
  self.setStyle = function () {
    self.defaults.styles.forEach(function eachStyle(i) {
      self.setDefaults(self.style, self.args.style || {}, i[0], i[1]);
    });
  };
  self.autosize = function (colName) {
    self.getVisibleSchema().forEach(function (col, colIndex) {
      if (col.name === colName || colName === undefined) {
        self.sizes.columns[colIndex] = Math.max(
          self.findColumnMaxTextLength(col.name),
          self.style.minColumnWidth,
        );
      }
    });
    self.sizes.columns[-1] = self.findColumnMaxTextLength('cornerCell');
  };
  self.dispose = function () {
    if (!self.isChildGrid && self.canvas && self.canvas.parentNode) {
      self.canvas.parentNode.removeChild(self.canvas);
    }
    if (!self.isChildGrid) {
      document.body.removeChild(self.controlInput);
    }
    self.eventParent.removeEventListener('mousedown', self.mousedown, false);
    self.eventParent.removeEventListener('dblclick', self.dblclick, false);
    self.eventParent.removeEventListener('click', self.click, false);
    self.eventParent.removeEventListener('mousemove', self.mousemove);
    self.eventParent.removeEventListener('wheel', self.scrollWheel, false);
    self.canvas.removeEventListener(
      'contextmenu',
      self.contextmenuEvent,
      false,
    );
    self.canvas.removeEventListener('copy', self.copy);
    self.controlInput.removeEventListener('copy', self.copy);
    self.controlInput.removeEventListener('cut', self.cut);
    self.controlInput.removeEventListener('paste', self.paste);
    self.controlInput.removeEventListener('keypress', self.keypress, false);
    self.controlInput.removeEventListener('keyup', self.keyup, false);
    self.controlInput.removeEventListener('keydown', self.keydown, false);
    window.removeEventListener('mouseup', self.mouseup, false);
    window.removeEventListener('mousemove', self.mousemove);
    window.removeEventListener('resize', self.resize);
    if (self.observer && self.observer.disconnect) {
      self.observer.disconnect();
    }
  };
  self.tryLoadStoredSettings = function () {
    var s;
    self.reloadStoredValues();
    if (
      self.storedSettings &&
      typeof self.storedSettings.orders === 'object' &&
      self.storedSettings.orders !== null
    ) {
      if (
        self.storedSettings.orders.rows.length >= (self.viewData || []).length
      ) {
        self.orders.rows = self.storedSettings.orders.rows;
      }
      s = self.getSchema();
      if (self.storedSettings.orders.columns.length === s.length) {
        self.orders.columns = self.storedSettings.orders.columns;
      }
      self.orderBy =
        self.storedSettings.orderBy === undefined
          ? s[0].name
          : self.storedSettings.orderBy;
      self.orderDirection =
        self.storedSettings.orderDirection === undefined
          ? 'asc'
          : self.storedSettings.orderDirection;
      if (
        self.storedSettings.orderBy !== undefined &&
        self.getHeaderByName(self.orderBy) &&
        self.orderDirection
      ) {
        self.order(self.orderBy, self.orderDirection);
      }
    }
  };
  self.toggleCollapseTree = function (rowIndex, columnIndex, type) {
    let tempData = [];
    let collapsedCount = 0;
    if (
      columnIndex == self.cellTree.rowTreeColIndex &&
      (rowIndex > 0 || (rowIndex == 0 && self.cellTree.rows[0].icon))
    ) {
      let ctr = self.cellTree.rows;
      switch (type) {
        case 'Expand':
          ctr[rowIndex].expand = true;
          self.cellTree.origin.rows[ctr[rowIndex].index].expand = true;
          break;

        case 'Collapse':
          ctr[rowIndex].expand = false;
          self.cellTree.origin.rows[ctr[rowIndex].index].expand = false;
          break;

        default:
          ctr[rowIndex].expand = !ctr[rowIndex].expand;
          self.cellTree.origin.rows[ctr[rowIndex].index].expand =
            ctr[rowIndex].expand;
      }
      for (
        let ri = ctr[rowIndex].index + 1;
        ri <= ctr[rowIndex].lastchild;
        ri++
      ) {
        let orTree = self.cellTree.origin.rows[ri];
        if (ctr[rowIndex].expand) {
          orTree.hide = false;
          if (orTree.icon && !orTree.expand) ri = orTree.lastchild;
        } else {
          orTree.hide = true;
        }
      }
    } else if (self.cellTree.columns[rowIndex]) {
      let ctc = self.cellTree.columns[rowIndex];

      switch (type) {
        case 'Expand':
          ctc[columnIndex].expand = true;
          break;

        case 'Collapse':
          ctc[columnIndex].expand = false;
          break;

        default:
          ctc[columnIndex].expand = !ctc[columnIndex].expand;
      }
      for (
        let ci = ctc[columnIndex].index + 1;
        ci <= ctc[columnIndex].lastchild;
        ci++
      ) {
        if (ctc[columnIndex].expand)
          self.cellTree.tempSchema[ci].hidden = false;
        else self.cellTree.tempSchema[ci].hidden = true;
      }
      let rc = 0,
        _ri;

      if (ctc[columnIndex].expand) {
        while (rc < ctc[columnIndex].child) {
          _ri = rowIndex + rc + 1;

          for (
            let _ci = ctc[columnIndex].index;
            _ci <= ctc[columnIndex].lastchild;
            _ci++
          ) {
            if (
              self.cellTree.origin.columns[_ri] &&
              self.cellTree.origin.columns[_ri][_ci].icon &&
              !self.cellTree.origin.columns[_ri][_ci].expand
            ) {
              for (
                let si = _ci + 1;
                si <= self.cellTree.origin.columns[_ri][_ci].lastchild;
                si++
              ) {
                self.cellTree.tempSchema[si].hidden = true;
              }
            }
          }

          rc++;
        }
      }
    }
    let otherData = {};
    let collapsed = [];
    self.cellTree.rows = [];
    self.cellTree.columns = {};
    for (let k in self.cellTree.origin.rows) {
      let tempRow = [];
      let tree = self.cellTree.origin.rows[k];
      if (!tree.hide) {
        let colTrees = [];
        let collapsedColCount = 0;
        if (k < self.cellTree.columnTreeRowStartIndex) {
          tempData.push(self.originalData[k]);
        } else {
          if (k > self.cellTree.columnTreeRowEndIndex) {
            otherData[k] = self.viewData[k];
            collapsedCount++;
          } else {
            for (let l = 0; l < self.originalData[k].length; l++) {
              if (!self.cellTree.tempSchema[l].hidden) {
                if (l < self.cellTree.rowTreeColIndex) {
                  if (!Object.prototype.hasOwnProperty.call(otherData, k))
                    otherData[k] = [];
                  otherData[k].push(self.viewData[k][l]);
                }
                tempRow.push(self.originalData[k][l]);
                if (
                  Object.prototype.hasOwnProperty.call(
                    self.cellTree.origin.columns,
                    k,
                  )
                )
                  colTrees.push(self.cellTree.origin.columns[k][l]);
              } else collapsedColCount++;
            }
            tempRow.push(...Array(collapsedColCount).fill(''));
            if (colTrees.length) {
              colTrees.push(
                ...Array(collapsedColCount)
                  .fill()
                  .map(() => {
                    return {};
                  }),
              );
              self.cellTree.columns[k] = colTrees;
            }
            tempData.push(tempRow);
          }
        }
        self.cellTree.rows.push(tree);
      } else {
        for (let l = 0; l < self.cellTree.rowTreeColIndex; l++) {
          tempRow.push(self.viewData[k][l]);
        }
        otherData[k] = tempRow;
        collapsed.push(Array(self.viewData[0].length).fill(''));
        collapsedCount++;
      }
    }
    if (collapsedCount) {
      self.cellTree.rows.push(
        ...Array(collapsedCount)
          .fill()
          .map((u, index) => {
            return { index: self.cellTree.rows.length + index };
          }),
      );
      tempData.push(...collapsed);
    }
    for (let k in otherData) {
      if (k > self.cellTree.columnTreeRowEndIndex) tempData[k] = otherData[k];
      else
        for (let l in otherData[k]) {
          tempData[k][l] = otherData[k][l];
        }
    }
    self.viewData = tempData;
  };
  self.cellTreeExpandCollapse = function (rowIndex, columnIndex, type) {
    if (
      columnIndex == self.cellTree.rowTreeColIndex &&
      (rowIndex > 0 || (rowIndex == 0 && self.cellTree.rows[0].icon))
    ) {
      const ctr = self.cellTree.rows;
      switch (type) {
        case 'Expand':
          ctr[rowIndex].expand = true;
          break;
        case 'Collapse':
          ctr[rowIndex].expand = false;
          break;
        default:
          ctr[rowIndex].expand = !ctr[rowIndex].expand;
      }
      for (let ri = rowIndex + 1; ri <= ctr[rowIndex].lastchild; ri++) {
        if (ctr[rowIndex].expand) {
          ctr[ri].hide = false;
          if (ctr[ri].icon && !ctr[ri].expand) ri = ctr[ri].lastchild;
        } else {
          ctr[ri].hide = true;
        }
      }
    } else if (self.cellTree.columns[rowIndex]) {
      const ctc = self.cellTree.columns[rowIndex];
      switch (type) {
        case 'Expand':
          ctc[columnIndex].expand = true;
          break;
        case 'Collapse':
          ctc[columnIndex].expand = false;
          break;
        default:
          ctc[columnIndex].expand = !ctc[columnIndex].expand;
      }

      for (let ci = columnIndex + 1; ci <= ctc[columnIndex].lastchild; ci++) {
        if (ctc[columnIndex].expand) self.tempSchema[ci].hidden = false;
        else self.tempSchema[ci].hidden = true;
      }

      let rc = 0,
        ri;
      if (ctc[columnIndex].expand) {
        while (rc < ctc[columnIndex].child) {
          ri = rowIndex + rc + 1;
          for (let ci = columnIndex; ci <= ctc[columnIndex].lastchild; ci++) {
            if (
              self.cellTree.columns[ri] &&
              self.cellTree.columns[ri][ci].icon &&
              !self.cellTree.columns[ri][ci].expand
            ) {
              for (
                let si = ci + 1;
                si <= self.cellTree.columns[ri][ci].lastchild;
                si++
              )
                self.tempSchema[si].hidden = true;
            }
          }
          rc++;
        }
      }
    }
  };

  self.initCellTreeSettings = function () {
    if (self.viewData === undefined) return;
    if (self.attributes.rowTree.length > 0 && self.viewData.length > 0) {
      self.cellTree.rows = Array(self.viewData.length)
        .fill()
        .map((u, index) => ({ index: index }));
      self.cellTree.rowTreeColIndex = self.attributes.rowTreeColIndex;
      let invalidRowTree = false;
      for (let rt of self.attributes.rowTree) {
        if (self.cellTree.rows.length <= rt.end) {
          invalidRowTree = true;
          break;
        }

        for (let ri = rt.begin; ri <= rt.end; ri++) {
          if (ri == rt.begin) {
            self.cellTree.rows[ri].icon = true;
            self.cellTree.rows[ri].lastchild = rt.end;
            self.cellTree.rows[ri].expand = true;
            if (!self.cellTree.rows[ri].parentCount)
              self.cellTree.rows[ri].parentCount = 0;
          } else {
            self.cellTree.rows[ri].hide = false;
            self.cellTree.rows[ri].parentIndex = rt.begin;
            if (self.cellTree.rows[ri] && self.cellTree.rows[ri].parentCount)
              self.cellTree.rows[ri].parentCount += 1;
            else self.cellTree.rows[ri].parentCount = 1;
          }
        }
      }
      if (invalidRowTree) self.cellTree.rows = {};
    }
    if (self.attributes.columnTree.length > 0 && self.viewData.length > 0) {
      self.cellTree.columnTreeRowStartIndex =
        self.attributes.columnTreeRowStartIndex;
      self.cellTree.columnTreeRowEndIndex =
        self.attributes.columnTreeRowEndIndex;
      let dataColumnLength = Object.keys(self.viewData[0]).length;
      let invalidColumnTree = false;
      for (let ct of self.attributes.columnTree) {
        if (dataColumnLength <= ct.end) {
          invalidColumnTree = true;
          break;
        }

        if (!self.cellTree.columns[ct.row])
          self.cellTree.columns[ct.row] = Array(dataColumnLength)
            .fill()
            .map((u, index) => ({ index: index }));

        for (let i = ct.begin; i <= ct.end; i++) {
          const ctc = self.cellTree.columns[ct.row][i];
          if (i == ct.begin) {
            ctc.icon = true;
            ctc.lastchild = ct.end;
            ctc.length = ct.end - ct.begin;
            ctc.expand = true;
            if (ct.child) ctc.child = ct.child;
            else ctc.child = 0;
          }
        }
      }
      self.cellTree.tempSchema = Array(dataColumnLength)
        .fill()
        .map(function () {
          return { hidden: false };
        });
      if (invalidColumnTree) self.cellTree.columns = {};
    }
    self.cellTree.origin = {
      rows: self.cellTree.rows,
      columns: self.cellTree.columns,
    };
  };

  self.getDomRoot = function () {
    return self.shadowRoot ? self.shadowRoot.host : self.parentNode;
  };
  self.getFontName = function (fontStyle) {
    return fontStyle.replace(/\d+\.?\d*px/, '');
  };
  self.getFontHeight = function (fontStyle) {
    return parseFloat(fontStyle);
  };
  self.parseStyleValue = function (key) {
    if (/Font/.test(key)) {
      self.style[key + 'Height'] = self.getFontHeight(self.style[key]);
      self.style[key + 'Name'] = self.getFontName(self.style[key]);
      return;
    }
    // when inheriting styles from already instantiated grids, don't parse already parsed values.
    if (
      key === 'moveOverlayBorderSegments' &&
      typeof self.style[key] === 'string'
    ) {
      self.style[key] = self.style[key].split(',').map(function (i) {
        return parseInt(i, 10);
      });
    }
  };
  self.initProp = function (propName) {
    if (!self.args[propName]) {
      return;
    }
    Object.keys(self.args[propName]).forEach(function (key) {
      self[propName][key] = self.args[propName][key];
    });
  };
  self.getStyleProperty = function (key) {
    if (self.styleKeys.indexOf(key) === -1) {
      return self.parentNodeStyle[key];
    }
    return self.style[key];
  };
  self.setStyleProperty = function (key, value, supressDrawAndEvent) {
    var isDim =
      [
        'height',
        'width',
        'minHeight',
        'minWidth',
        'maxHeight',
        'maxWidth',
      ].indexOf(key) !== -1;
    if (self.styleKeys.indexOf(key) === -1) {
      self.parentNodeStyle[key] = value;
    } else {
      if (/-/.test(key)) {
        key = self.dehyphenateProperty(key);
      }
      self.style[key] = value;
      self.parseStyleValue(key);
    }
    if (isDim) {
      self.resize();
    }
    if (!supressDrawAndEvent) {
      self.draw(true);
      self.dispatchEvent('stylechanged', { name: 'style', value: value });
    }
  };
  self.reloadStoredValues = function () {
    if (self.attributes.name && self.attributes.saveAppearance) {
      try {
        self.storedSettings = localStorage.getItem(
          self.storageName + '-' + self.attributes.name,
        );
      } catch (e) {
        console.warn('Error loading stored values. ' + e.message);
        self.storedSettings = undefined;
      }
      if (self.storedSettings) {
        try {
          self.storedSettings = JSON.parse(self.storedSettings);
        } catch (e) {
          console.warn('could not read settings from localStore', e);
          self.storedSettings = undefined;
        }
      }
      if (self.storedSettings) {
        if (
          typeof self.storedSettings.sizes === 'object' &&
          self.storedSettings.sizes !== null
        ) {
          self.sizes.rows = self.storedSettings.sizes.rows;
          self.sizes.columns = self.storedSettings.sizes.columns;
          ['trees', 'columns', 'rows'].forEach(function (i) {
            if (!self.sizes[i]) {
              self.sizes[i] = {};
            }
          });
        }
        if (typeof self.storedSettings.visibility === 'object') {
          self.getSchema().forEach(function (column) {
            if (
              self.storedSettings.visibility &&
              self.storedSettings.visibility[column.name] !== undefined
            ) {
              column.hidden = !self.storedSettings.visibility[column.name];
            }
          });
        }
      }
    }
  };
  self.init = function () {
    if (self.initialized) {
      return;
    }
    function addStyleKeyIfNoneExists(key) {
      if (self.styleKeys.indexOf(key) === -1) {
        self.styleKeys.push(key);
      }
    }
    var publicStyleKeyIntf = {};
    self.setAttributes();
    self.setStyle();
    self.initScrollBox();
    self.setDom();
    self.nodeType = 'canvas-datagrid';
    self.ie = /Trident/.test(window.navigator.userAgent);
    self.edge = /Edge/.test(window.navigator.userAgent);
    self.webKit = /WebKit/.test(window.navigator.userAgent);
    self.moz = /Gecko/.test(window.navigator.userAgent);
    self.mobile = /Mobile/i.test(window.navigator.userAgent);
    self.blankValues = [undefined, null, ''];
    self.cursorGrab = 'grab';
    self.cursorGrabing = 'grabbing';
    self.cursorGrab = self.webKit ? '-webkit-grab' : self.cursorGrab;
    self.cursorGrabing = self.moz ? '-webkit-grabbing' : self.cursorGrabbing;
    self.pointerLockPosition = { x: 0, y: 0 };
    Object.keys(self.style).forEach(self.parseStyleValue);
    self.intf.moveSelection = self.moveSelection;
    self.intf.deleteSelectedData = self.deleteSelectedData;
    self.intf.moveTo = self.moveTo;
    self.intf.addEventListener = self.addEventListener;
    self.intf.removeEventListener = self.removeEventListener;
    self.intf.dispatchEvent = self.dispatchEvent;
    /**
     * Releases grid resources and removes grid elements.
     * @memberof canvasDatagrid
     * @name dispose
     * @method
     */
    self.intf.dispose = self.dispose;
    /**
     * Appends the grid to another element later.  Not implemented.
     * @memberof canvasDatagrid
     * @name appendTo
     * @method
     * @param {number} el The element to append the grid to.
     */
    self.intf.appendTo = self.appendTo;
    self.intf.getVisibleCellByIndex = self.getVisibleCellByIndex;
    self.intf.filters = self.filters;
    self.intf.sorters = self.sorters;
    self.intf.autosize = self.autosize;
    self.intf.beginEditAt = self.beginEditAt;
    self.intf.endEdit = self.endEdit;
    self.intf.setActiveCell = self.setActiveCell;
    self.intf.forEachSelectedCell = self.forEachSelectedCell;
    self.intf.scrollIntoView = self.scrollIntoView;
    self.intf.clearChangeLog = self.clearChangeLog;
    self.intf.gotoCell = self.gotoCell;
    self.intf.gotoRow = self.gotoRow;
    self.intf.addButton = self.addButton;
    self.intf.toggleCellCollapseTree = self.toggleCellCollapseTree;
    self.intf.expandCollapseCellTree = self.expandCollapseCellTree;
    self.intf.getHeaderByName = self.getHeaderByName;
    self.intf.findColumnScrollLeft = self.findColumnScrollLeft;
    self.intf.findRowScrollTop = self.findRowScrollTop;
    self.intf.fitColumnToValues = self.fitColumnToValues;
    self.intf.findColumnMaxTextLength = self.findColumnMaxTextLength;
    self.intf.disposeContextMenu = self.disposeContextMenu;
    self.intf.getCellAt = self.getCellAt;
    self.intf.groupColumns = self.groupColumns;
    self.intf.groupRows = self.groupRows;
    self.intf.removeGroupColumns = self.removeGroupColumns;
    self.intf.removeGroupRows = self.removeGroupRows;
    self.intf.toggleGroupColumns = self.toggleGroupColumns;
    self.intf.toggleGroupRows = self.toggleGroupRows;
    self.intf.getGroupsColumnBelongsTo = self.getGroupsColumnBelongsTo;
    self.intf.getGroupsRowBelongsTo = self.getGroupsRowBelongsTo;
    self.intf.isCellVisible = self.isCellVisible;
    self.intf.isRowVisible = self.isRowVisible;
    self.intf.isColumnVisible = self.isColumnVisible;
    self.intf.order = self.order;
    self.intf.draw = self.draw;
    self.intf.refresh = self.refresh;
    self.intf.isComponent = self.isComponent;
    self.intf.selectArea = self.selectArea;
    self.intf.clipElement = self.clipElement;
    self.intf.getSchemaFromData = self.getSchemaFromData;
    self.intf.setFilter = self.setFilter;
    self.intf.parentGrid = self.parentGrid;
    self.intf.toggleTree = self.toggleTree;
    self.intf.expandTree = self.expandTree;
    self.intf.collapseTree = self.collapseTree;
    self.intf.canvas = self.canvas;
    self.intf.context = self.ctx;
    self.intf.insertRow = self.insertRow;
    self.intf.deleteRow = self.deleteRow;
    self.intf.addRow = self.addRow;
    self.intf.insertColumn = self.insertColumn;
    self.intf.deleteColumn = self.deleteColumn;
    self.intf.addColumn = self.addColumn;
    self.intf.getClippingRect = self.getClippingRect;
    self.intf.setRowHeight = self.setRowHeight;
    self.intf.setColumnWidth = self.setColumnWidth;
    self.intf.resetColumnWidths = self.resetColumnWidths;
    self.intf.resetRowHeights = self.resetRowHeights;
    self.intf.resize = self.resize;
    self.intf.selectColumn = self.selectColumn;
    self.intf.selectRow = self.selectRow;
    self.intf.selectAll = self.selectAll;
    self.intf.selectNone = self.selectNone;
    self.intf.drawChildGrids = self.drawChildGrids;
    self.intf.assertPxColor = self.assertPxColor;
    self.intf.clearPxColorAssertions = self.clearPxColorAssertions;
    self.intf.integerToAlpha = self.integerToAlpha;
    self.intf.copy = self.copy;
    self.intf.cut = self.cut;
    self.intf.paste = self.paste;
    self.intf.setStyleProperty = self.setStyleProperty;
    self.intf.hideColumns = self.hideColumns;
    self.intf.unhideColumns = self.unhideColumns;
    self.intf.hideRows = self.hideRows;
    self.intf.unhideRows = self.unhideRows;
    Object.defineProperty(self.intf, 'defaults', {
      get: function () {
        return {
          styles: self.defaults.styles.reduce(function (a, i) {
            a[i[0]] = i[1];
            return a;
          }, {}),
          attributes: self.defaults.attributes.reduce(function (a, i) {
            a[i[0]] = i[1];
            return a;
          }, {}),
        };
      },
    });
    self.styleKeys = Object.keys(self.intf.defaults.styles);
    self.styleKeys
      .map(function (i) {
        return self.hyphenateProperty(i, false);
      })
      .forEach(addStyleKeyIfNoneExists);
    self.styleKeys
      .map(function (i) {
        return self.hyphenateProperty(i, true);
      })
      .forEach(addStyleKeyIfNoneExists);
    self.DOMStyles = window.getComputedStyle(document.body, null);
    self.styleKeys.concat(Object.keys(self.DOMStyles)).forEach(function (key) {
      // unless this line is here, Object.keys() will not work on <instance>.style
      publicStyleKeyIntf[key] = undefined;
      Object.defineProperty(publicStyleKeyIntf, key, {
        get: function () {
          return self.getStyleProperty(key);
        },
        set: function (value) {
          if (self.initialized) {
            self.appliedInlineStyles[key] = value;
          }
          self.setStyleProperty(key, value);
        },
      });
    });
    Object.defineProperty(self.intf, 'shadowRoot', {
      get: function () {
        return self.shadowRoot;
      },
    });
    Object.defineProperty(self.intf, 'activeCell', {
      get: function () {
        return self.activeCell;
      },
    });
    Object.defineProperty(self.intf, 'hasFocus', {
      get: function () {
        return self.hasFocus;
      },
    });
    Object.defineProperty(self.intf, 'hasActiveFilters', {
      get: function () {
        return self.hasActiveFilters();
      },
    });
    Object.defineProperty(self.intf, 'style', {
      get: function () {
        return publicStyleKeyIntf;
      },
      set: function (valueObject) {
        Object.keys(valueObject).forEach(function (key) {
          self.setStyleProperty(key, valueObject[key], true);
        });
        self.draw(true);
        self.dispatchEvent('stylechanged', {
          name: 'style',
          value: valueObject,
        });
      },
    });
    Object.defineProperty(self.intf, 'attributes', { value: {} });
    Object.keys(self.attributes).forEach(function (key) {
      Object.defineProperty(self.intf.attributes, key, {
        get: function () {
          return self.attributes[key];
        },
        set: function (value) {
          self.attributes[key] = value;
          if (key === 'name') {
            self.tryLoadStoredSettings();
          }
          if (
            key === 'rowTree' ||
            key === 'columnTree' ||
            key === 'columnTreeRowEndIndex'
          ) {
            self.initCellTreeSettings();
          }
          self.draw(true);
          self.dispatchEvent('attributechanged', {
            name: key,
            value: value[key],
          });
        },
      });
    });
    self.filters.string = function (value, filterFor) {
      if (filterFor === self.attributes.blanksText) {
        return self.blankValues.includes(
          value == null ? value : String(value).trim(),
        );
      }

      value = String(value);
      var filterRegExp,
        regEnd = /\/(i|g|m)*$/,
        pattern = regEnd.exec(filterFor),
        flags = pattern ? pattern[0].substring(1) : '',
        flagLength = flags.length;
      self.invalidFilterRegEx = undefined;
      if (filterFor.substring(0, 1) === '/' && pattern) {
        try {
          filterRegExp = new RegExp(
            filterFor.substring(1, filterFor.length - (flagLength + 1)),
            flags,
          );
        } catch (e) {
          self.invalidFilterRegEx = e;
          return;
        }
        return filterRegExp.test(value);
      }
      return value.toString
        ? value
            .toString()
            .toLocaleUpperCase()
            .indexOf(filterFor.toLocaleUpperCase()) !== -1
        : false;
    };
    self.filters.number = function (value, filterFor) {
      if (filterFor === self.attributes.blanksText) {
        return self.blankValues.includes(
          value == null ? value : String(value).trim(),
        );
      }

      if (!filterFor) {
        return true;
      }
      return value === filterFor;
    };
    ['formatters', 'filters', 'sorters'].forEach(self.initProp);
    self.applyComponentStyle(false, self.intf);
    self.reloadStoredValues();
    if (self.args.data) {
      self.intf.data = self.args.data;
    }
    if (self.intf.innerText || self.intf.textContent) {
      if (self.intf.dataType === 'application/x-canvas-datagrid') {
        self.intf.dataType = 'application/json+x-canvas-datagrid';
      }
      self.intf.data = self.intf.innerText || self.intf.textContent;
    }
    if (self.args.schema) {
      self.intf.schema = self.args.schema;
    }
    if (self.isChildGrid || !self.isComponent) {
      requestAnimationFrame(function () {
        self.resize(true);
      });
    } else {
      self.resize(true);
    }
    self.initialized = true;
    return self;
  };
  /**
   * Removes focus from the grid.
   * @memberof canvasDatagrid
   * @name blur
   * @method
   */
  self.intf.blur = function (e) {
    self.hasFocus = false;
  };
  /**
   * Focuses on the grid.
   * @memberof canvasDatagrid
   * @name focus
   * @method
   */
  self.intf.focus = function () {
    self.hasFocus = true;
    self.controlInput.focus();
  };
  if (self.shadowRoot || self.isChildGrid) {
    Object.defineProperty(self.intf, 'height', {
      get: function () {
        if (self.shadowRoot) {
          return self.shadowRoot.height;
        }
        return self.parentNode.height;
      },
      set: function (value) {
        if (self.shadowRoot) {
          self.shadowRoot.height = value;
        } else {
          self.parentNode.height = value;
        }
        self.resize(true);
      },
    });
    Object.defineProperty(self.intf, 'width', {
      get: function () {
        if (self.shadowRoot) {
          return self.shadowRoot.width;
        }
        return self.parentNode.width;
      },
      set: function (value) {
        if (self.shadowRoot) {
          self.shadowRoot.width = value;
        } else {
          self.parentNode.width = value;
        }
        self.resize(true);
      },
    });
    Object.defineProperty(self.intf, 'parentNode', {
      get: function () {
        return self.parentNode;
      },
      set: function (value) {
        if (!self.isChildGrid) {
          throw new TypeError(
            'Cannot set property parentNode which has only a getter',
          );
        }
        self.parentNode = value;
      },
    });
  }
  Object.defineProperty(self.intf, 'visibleRowHeights', {
    get: function () {
      return self.visibleRowHeights;
    },
  });
  Object.defineProperty(self.intf, 'openChildren', {
    get: function () {
      return self.openChildren;
    },
  });
  Object.defineProperty(self.intf, 'childGrids', {
    get: function () {
      return Object.keys(self.childGrids).map(function (gridId) {
        return self.childGrids[gridId];
      });
    },
  });
  Object.defineProperty(self.intf, 'isChildGrid', {
    get: function () {
      return self.isChildGrid;
    },
  });
  Object.defineProperty(self, 'cursor', {
    get: function () {
      return self.parentNodeStyle.cursor;
    },
    set: function (value) {
      if (value === 'cell') {
        value = 'default';
      }
      if (self.currentCursor !== value) {
        self.parentNodeStyle.cursor = value;
        self.currentCursor = value;
      }
    },
  });
  Object.defineProperty(self.intf, 'orderDirection', {
    get: function () {
      return self.orderDirection;
    },
    set: function (value) {
      if (value !== 'desc') {
        value = 'asc';
      }
      self.orderDirection = value;
      self.order(self.orderBy, self.orderDirection);
    },
  });
  Object.defineProperty(self.intf, 'orderBy', {
    get: function () {
      return self.orderBy;
    },
    set: function (value) {
      if (
        self.getSchema().find(function (col) {
          return col.name === value;
        }) === undefined
      ) {
        throw new Error('Cannot sort by unknown column name.');
      }
      self.orderBy = value;
      self.order(self.orderBy, self.orderDirection);
    },
  });
  if (self.isComponent) {
    Object.defineProperty(self.intf, 'offsetHeight', {
      get: function () {
        return self.canvas.offsetHeight;
      },
    });
    Object.defineProperty(self.intf, 'offsetWidth', {
      get: function () {
        return self.canvas.offsetWidth;
      },
    });
  }
  Object.defineProperty(self.intf, 'scrollHeight', {
    get: function () {
      return self.scrollBox.scrollHeight;
    },
  });
  Object.defineProperty(self.intf, 'scrollWidth', {
    get: function () {
      return self.scrollBox.scrollWidth;
    },
  });
  Object.defineProperty(self.intf, 'scrollTop', {
    get: function () {
      return self.scrollBox.scrollTop;
    },
    set: function (value) {
      self.scrollBox.scrollTop = value;
    },
  });
  Object.defineProperty(self.intf, 'scrollLeft', {
    get: function () {
      return self.scrollBox.scrollLeft;
    },
    set: function (value) {
      self.scrollBox.scrollLeft = value;
    },
  });
  Object.defineProperty(self.intf, 'sizes', {
    get: function () {
      return self.sizes;
    },
  });
  Object.defineProperty(self.intf, 'parentDOMNode', {
    get: function () {
      return self.parentDOMNode;
    },
  });
  Object.defineProperty(self.intf, 'input', {
    get: function () {
      return self.input;
    },
  });
  Object.defineProperty(self.intf, 'controlInput', {
    get: function () {
      return self.controlInput;
    },
  });
  Object.defineProperty(self.intf, 'currentCell', {
    get: function () {
      return self.currentCell;
    },
  });
  Object.defineProperty(self.intf, 'visibleCells', {
    get: function () {
      return self.visibleCells;
    },
  });
  Object.defineProperty(self.intf, 'visibleRows', {
    get: function () {
      return self.visibleRows;
    },
  });
  let warnedForObsoleteSelections;
  Object.defineProperty(self.intf, 'selections', {
    get: function () {
      if (!warnedForObsoleteSelections) {
        console.warn(
          `DeprecationWarning: The property 'selections' is deprecated due to performance issue. ` +
            `And it will be removed in the future.` +
            `Please use new property 'selectionList' instead. ` +
            `Visit this link to get more information: ` +
            `https://github.com/TonyGermaneri/canvas-datagrid/pull/498`,
        );
        warnedForObsoleteSelections = true;
      }
      return self.getObsoleteSelectionMatrix();
    },
  });
  Object.defineProperty(self.intf, 'selectionList', {
    get: function () {
      return self.selections;
    },
  });
  Object.defineProperty(self.intf, 'dragMode', {
    get: function () {
      return self.dragMode;
    },
  });
  Object.defineProperty(self.intf, 'changes', {
    get: function () {
      return self.changes;
    },
  });
  self.intf.formatters = self.formatters;
  Object.defineProperty(self.intf, 'dataType', {
    get: function () {
      return self.dataType;
    },
    set: function (value) {
      if (!self.parsers[value]) {
        throw new Error('No parser for MIME type ' + value);
      }
      self.dataType = value;
    },
  });
  self.eventNames.forEach(function (eventName) {
    Object.defineProperty(self.intf, 'on' + eventName, {
      get: function () {
        return self.componentL1Events[eventName];
      },
      set: function (value) {
        self.events[eventName] = [];
        self.componentL1Events[eventName] = value;
        if (!value) {
          return;
        }
        self.addEventListener(eventName, value);
      },
    });
  });
  Object.defineProperty(self.intf, 'frozenRow', {
    get: function () {
      return self.frozenRow;
    },
    set: function (val) {
      if (isNaN(val)) {
        throw new TypeError('Expected value for frozenRow to be a number.');
      }
      if (self.visibleRows.length < val) {
        throw new RangeError(
          'Cannot set a value larger than the number of visible rows.',
        );
      }
      self.frozenRow = val;
    },
  });
  Object.defineProperty(self.intf, 'frozenColumn', {
    get: function () {
      return self.frozenColumn;
    },
    set: function (val) {
      if (isNaN(val)) {
        throw new TypeError('Expected value for frozenRow to be a number.');
      }
      if (self.getVisibleSchema().length < val) {
        throw new RangeError(
          'Cannot set a value larger than the number of visible columns.',
        );
      }
      self.frozenColumn = val;
    },
  });
  Object.defineProperty(self.intf, 'scrollIndexRect', {
    get: function () {
      return {
        top: self.scrollIndexTop,
        right: self.scrollIndexRight,
        bottom: self.scrollIndexBottom,
        left: self.scrollIndexLeft,
      };
    },
  });
  Object.defineProperty(self.intf, 'scrollPixelRect', {
    get: function () {
      return {
        top: self.scrollPixelTop,
        right: self.scrollPixelRight,
        bottom: self.scrollPixelBottom,
        left: self.scrollPixelLeft,
      };
    },
  });
  Object.defineProperty(self.intf, 'rowOrder', {
    get: function () {
      return self.orders.rows;
    },
    set: function (val) {
      if (!Array.isArray(val)) {
        throw new TypeError('Value must be an array.');
      }
      if (!self.originalData || val.length < self.originalData.length) {
        throw new RangeError(
          'Array length must be equal to or greater than number of rows.',
        );
      }
      self.orders.rows = val;
    },
  });
  Object.defineProperty(self.intf, 'columnOrder', {
    get: function () {
      return self.orders.columns;
    },
    set: function (val) {
      if (!Array.isArray(val)) {
        throw new TypeError('Value must be an array.');
      }
      if (val.length < self.getSchema().length) {
        throw new RangeError(
          'Array length must be equal to or greater than number of columns.',
        );
      }
      self.orders.columns = val;
    },
  });
  Object.defineProperty(self.intf, 'selectionBounds', {
    get: function () {
      return self.getSelectionBounds();
    },
  });
  Object.defineProperty(self.intf, 'selectedRows', {
    get: function () {
      return self.getSelectedData(true);
    },
  });
  Object.defineProperty(self.intf, 'selectedCells', {
    get: function () {
      return self.getSelectedData();
    },
  });
  Object.defineProperty(self.intf, 'visibleSchema', {
    get: function () {
      return self.getVisibleSchema().map(function eachDataRow(col) {
        return col;
      });
    },
  });
  Object.defineProperty(self.intf, 'treeGridAttributes', {
    get: function () {
      return self.treeGridAttributes;
    },
    set: function setTreeGridAttributes(value) {
      self.treeGridAttributes = value;
    },
  });
  Object.defineProperty(self.intf, 'cellGridAttributes', {
    get: function () {
      return self.cellGridAttributes;
    },
    set: function setCellGridAttributes(value) {
      self.cellGridAttributes = value;
    },
  });
  Object.defineProperty(self.intf, 'fillCellCallback', {
    get: function () {
      return self.fillCellCallback;
    },
    set: function setFillCellCallback(value) {
      self.fillCellCallback = value;
    },
  });
  Object.defineProperty(self.intf, 'ctx', {
    get: function () {
      return self.ctx;
    },
  });
  Object.defineProperty(self.intf, 'schema', {
    get: function schemaGetter() {
      return self.getSchema();
    },
    set: function schemaSetter(value) {
      if (value === undefined) {
        // Issue #89 - allow schema to be set to initialized state
        self.schema = undefined;
        self.tempSchema = undefined;
        self.dispatchEvent('schemachanged', { schema: undefined });
        return;
      }
      if (!Array.isArray(value) || typeof value[0] !== 'object') {
        throw new Error('Schema must be an array of objects.');
      }
      if (value[0].name === undefined) {
        throw new Error(
          'Expected schema to contain an object with at least a name property.',
        );
      }
      self.schema = value.map(function eachSchemaColumn(column, index) {
        column.width = column.width || self.style.cellWidth;
        column.filter = column.filter || self.filter(column.type);
        column.type = column.type || 'string';
        column.index = index;
        column.columnIndex = index;
        column.rowIndex = -1;
        return column;
      });
      self.tempSchema = undefined;
      self.createNewRowData();
      self.createColumnOrders();
      self.tryLoadStoredSettings();
      if (
        self.storedSettings &&
        typeof self.storedSettings.visibility === 'object'
      ) {
        self.schema.forEach(function hideEachSchemaColumn(column, index) {
          if (
            self.storedSettings &&
            self.storedSettings.visibility[column.name] !== undefined
          ) {
            column.hidden = !self.storedSettings.visibility[column.name];
          }
        });
      }
      self.resize(true);
      self.dispatchEvent('schemachanged', { schema: self.schema });
    },
  });
  /**
   * Gets an array of currently registered MIME types.
   * @memberof canvasDatagrid
   * @name getDataTypes
   * @method
   */
  self.intf.getTypes = function () {
    return Object.keys(self.parsers);
  };
  self.parseInnerHtml = function (data) {
    if (!data || /^ +$/.test(data)) {
      return [];
    }
    try {
      data = JSON.parse(data);
    } catch (e) {
      console.warn(
        Error(
          'Cannot parse application/json+x-canvas-datagrid formated data. ' +
            e.message +
            '  \nNote: canvas-datagrid.innerHTML is for string data only.  ' +
            'Use the canvas-datagrid.data property to set object data.',
        ),
      );
    }
    return data;
  };
  self.parsers['application/json+x-canvas-datagrid'] = function (
    data,
    callback,
  ) {
    self.parsers['application/x-canvas-datagrid'](
      self.parseInnerHtml(data),
      function (data, schema) {
        return callback(data, schema);
      },
    );
  };
  self.parsers['application/x-canvas-datagrid'] = function (data, callback) {
    return callback(data);
  };
  self.intf.parsers = self.parsers;
  // send to dataType ETL function to extract from input data
  // and transform into native [{}, {}] format
  self.etl = function (data, callback) {
    if (!self.intf.parsers[self.dataType]) {
      throw new Error('Unsupported data type.');
    }
    self.intf.parsers[self.dataType](data, function (data, schema) {
      // set the unfiltered/sorted data array
      self.originalData = data;
      self.viewData = Array.from(self.originalData);

      if (Array.isArray(schema)) {
        self.schema = schema;
      }
      // Issue #89 - allow schema to be auto-created every time data is set
      if (self.attributes.autoGenerateSchema) {
        self.schema = self.getSchemaFromData(data);
      }
      if (!self.schema) {
        self.tempSchema = self.getSchemaFromData(data);
      }
      if (self.getSchema()) {
        self.createColumnOrders();
      }
      // apply filter, sort, etc to incoming dataset, set viewData:
      self.refresh();
      // empty data was set
      if (!self.schema && (self.originalData || []).length === 0) {
        self.tempSchema = [{ name: '' }];
      }
      self.fitColumnToValues('cornerCell', true);
      if (
        (self.tempSchema && !self.schema) ||
        self.attributes.autoGenerateSchema
      ) {
        self.createColumnOrders();
        self.dispatchEvent('schemachanged', { schema: self.tempSchema });
      }
      callback();
    });
  };
  Object.defineProperty(self.intf, 'viewData', {
    get: function () {
      return self.viewData;
    },
  });
  Object.defineProperty(self.intf, 'boundData', {
    get: function () {
      return self.originalData;
    },
  });
  Object.defineProperty(self.intf, 'data', {
    get: function dataGetter() {
      return self.originalData;
    },
    set: function dataSetter(value) {
      self.etl(value, function () {
        self.changes = [];
        self.createNewRowData();
        if (
          self.attributes.autoResizeColumns &&
          self.originalData.length > 0 &&
          self.storedSettings === undefined
        ) {
          self.autosize();
        }
        // set the header column to fit the numbers in it
        self.fitColumnToValues('cornerCell', true);
        self.createRowOrders();
        self.tryLoadStoredSettings();
        self.dispatchEvent('datachanged', { data: self.originalData });
        self.initCellTreeSettings();
        self.resize(true);
      });
    },
  });
  self.initScrollBox = function () {
    var sHeight = 0,
      sWidth = 0,
      scrollTop = 0,
      scrollLeft = 0,
      scrollHeight = 0,
      scrollWidth = 0,
      scrollBoxHeight = 20,
      scrollBoxWidth = 20;
    function setScrollTop(value, preventScrollEvent) {
      if (isNaN(value)) {
        throw new Error('ScrollTop value must be a number');
      }
      if (value < 0) {
        value = 0;
      }
      if (value > scrollHeight) {
        value = scrollHeight;
      }
      if (scrollHeight < 0) {
        value = 0;
      }
      scrollTop = value;
      if (!preventScrollEvent) {
        self.scroll();
      }
      if (self.button) {
        self.moveButtonPos();
      }
    }
    function setScrollLeft(value, preventScrollEvent) {
      if (isNaN(value)) {
        throw new Error('ScrollLeft value must be a number');
      }
      if (value < 0) {
        value = 0;
      }
      if (value > scrollWidth) {
        value = scrollWidth;
      }
      if (scrollWidth < 0) {
        value = 0;
      }
      scrollLeft = value;
      if (!preventScrollEvent) {
        self.scroll();
      }
      if (self.button) {
        self.moveButtonPos();
      }
    }
    self.scrollBox.toString = function () {
      return (
        '{"width": ' +
        scrollWidth.toFixed(2) +
        ', "height": ' +
        scrollHeight.toFixed(2) +
        ', "left": ' +
        scrollLeft.toFixed(2) +
        ', "top": ' +
        scrollTop.toFixed(2) +
        ', "widthRatio": ' +
        self.scrollBox.widthBoxRatio.toFixed(5) +
        ', "heightRatio": ' +
        self.scrollBox.heightBoxRatio.toFixed(5) +
        '}'
      );
    };
    self.scrollBox.scrollTo = function (x, y, supressDrawEvent) {
      setScrollLeft(x, true);
      setScrollTop(y, supressDrawEvent);
    };
    Object.defineProperty(self.scrollBox, 'scrollBoxHeight', {
      get: function () {
        return scrollBoxHeight;
      },
      set: function (value) {
        scrollBoxHeight = value;
      },
    });
    Object.defineProperty(self.scrollBox, 'scrollBoxWidth', {
      get: function () {
        return scrollBoxWidth;
      },
      set: function (value) {
        scrollBoxWidth = value;
      },
    });
    Object.defineProperty(self.scrollBox, 'height', {
      get: function () {
        return sHeight;
      },
      set: function (value) {
        sHeight = value;
      },
    });
    Object.defineProperty(self.scrollBox, 'width', {
      get: function () {
        return sWidth;
      },
      set: function (value) {
        sWidth = value;
      },
    });
    Object.defineProperty(self.scrollBox, 'scrollTop', {
      get: function () {
        return scrollTop;
      },
      set: setScrollTop,
    });
    Object.defineProperty(self.scrollBox, 'scrollLeft', {
      get: function () {
        return scrollLeft;
      },
      set: setScrollLeft,
    });
    Object.defineProperty(self.scrollBox, 'scrollHeight', {
      get: function () {
        return scrollHeight;
      },
      set: function (value) {
        if (scrollTop > value) {
          scrollTop = Math.max(value, 0);
        }
        scrollHeight = value;
      },
    });
    Object.defineProperty(self.scrollBox, 'scrollWidth', {
      get: function () {
        return scrollWidth;
      },
      set: function (value) {
        if (scrollLeft > value) {
          scrollLeft = Math.max(value, 0);
        }
        scrollWidth = value;
      },
    });
  };
  return;
}
